package at.fhooe.tuxgestures.cam.mapper;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EventObject;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;

import at.fhooe.tuxgestures.cam.data.CameraDataProvider;
import at.fhooe.tuxgestures.cam.data.event.DataEvent;
import at.fhooe.tuxgestures.cam.data.event.IDataEventListener;
import at.fhooe.tuxgestures.cam.mapper.IMovementPoint.MovementPointType;
import at.fhooe.tuxgestures.cam.mapper.event.IStateChangeEventListener;
import at.fhooe.tuxgestures.cam.mapper.event.StateChangeEvent;
import at.fhooe.tuxgestures.cam.resources.Resources;
import at.fhooe.tuxgestures.keysimulator.IKeySimulator.State;

public class CameraDataMovementMapper implements ICameraDataMovementMapper
{
	// Might not be needed, some kind of calibratoin might
	// solve the problem
//	List<DataEvent> dataEventBuffer = new ArrayList<DataEvent>();
	
	private static final int ARR_WIDTH = Resources.getCalibrationIntValue("ARR_WIDTH");
	private static final int ARR_HEIGHT = Resources.getCalibrationIntValue("ARR_HEIGHT");
	
	private static final int BLUR_HALF = Resources.getCalibrationIntValue("BLUR_HALF");
	private static final int INGAME_RETRIES = Resources.getCalibrationIntValue("INGAME_RETRIES");
	private static final int ANALYZE_CANDIDATE = Resources.getCalibrationIntValue("ANALYZE_CANDIDATE");
	private static final int FORWARD_CHECK = Resources.getCalibrationIntValue("FORWARD_CHECK");
	private static final int START_GAME_BUFF = Resources.getCalibrationIntValue("START_GAME_BUFF");
	private int retryCounter = 0;
	private int startGameCounter = 0;
	
	private List<IStateChangeEventListener> listeners = new ArrayList<IStateChangeEventListener>();
	
	private Comparator<IMovementPoint> movementPointLeftComparator = 
			new Comparator<IMovementPoint>()
	{
		
		@Override
		public int compare(IMovementPoint mp1, IMovementPoint mp2)
		{
			// the lower width element is better and should
			// be ordered first
			return Integer.compare(mp1.getWidthIndex(), mp2.getWidthIndex());
		}
	};
	
	private Comparator<IMovementPoint> movementPointRightComparator = 
			new Comparator<IMovementPoint>()
	{
				
		@Override
		public int compare(IMovementPoint mp1, IMovementPoint mp2)
		{
			// the higher width element is better and should 
			// be ordered first
			if (mp1.getWidthIndex() > mp2.getWidthIndex())
				return -1;
			else if (mp1.getWidthIndex() == mp2.getWidthIndex())
				return 0;
			else return 1;
		}
	};
	
	private PriorityQueue<IMovementPoint> movementPointsLeft = 
			new PriorityQueue<>(200, movementPointLeftComparator);
	private PriorityQueue<IMovementPoint> movementPointsRight = 
			new PriorityQueue<>(200, movementPointRightComparator);
	
	private float[][] matrix = null;
	
	private boolean inGame = false;
	
//	
//	/**
//	 * The current movement points. This map is filled with the movement
//	 * points defined in {@link BodyPart}.
//	 */
//	private Map<BodyPart, IMovementPoint> currMovementPointMap = null;
//	/**
//	 * Stores the movements of the current and previous analysis.
//	 * At the moment this is just used with an Integer, so one can 
//	 * define how big the buffer should be and then it remembers 
//	 * a certain number of movement maps (a movement map contains
//	 * the movement patterns for.
//	 */
//	private Map<Integer, Map<BodyPart, IMovementPoint>> movementBuffer = null;
//	private int movementBufferSize = 2; // only remember the previous movement
	
	private State prevState = null;
	
	/**
	 * Analyzes if this is a valid movement point or just some noise.
	 * 
	 * @param point the movement point to be checked
	 * @return true if the movement point is valid, false otherwise
	 */
	private boolean analyzeMovementPointCandidate(IMovementPoint point)
	{
		if (matrix != null)
		{
			// check the area around the movement point in the matrix
			// if we find stuff around return true
			int widthIndex = point.getWidthIndex();
			int heightIndex = point.getHeightIndex();
			
			boolean ok = false;
			
			// TODO: Check if width and height are used correctly here...
			switch (point.getMovementPointType())
			{
				// check always 2 left of right
				case LEFT:
				{
					// check ANALYZE_CANDIDATE fields right
					for (int i = 1; i <= ANALYZE_CANDIDATE; i++)
					{
						if (widthIndex + i < ARR_WIDTH)
						{
							
							ok = ((int)matrix[widthIndex + i][heightIndex]) == 1;
						}
						else
						{
							ok = false;
							break;
						}
					}
					return ok;

				}
				case RIGHT:
				{
					for (int i = 1; i <= ANALYZE_CANDIDATE; i++)
					{
						if (widthIndex - i >= 0)
						{
							
							ok = ((int)matrix[widthIndex - i][heightIndex]) == 1;
						}
						else
						{
							ok = false;
							break;
						}
					}
					return ok;
				}

				default:
					break;
			}
		}
		
		return false;
	}
	
	private State mapMovementPointsToState(IMovementPoint left, IMovementPoint right)
	{	
		int leftHeightPos = left.getHeightIndex();
		int rightHeightPost = right.getHeightIndex();
		
		if ((leftHeightPos <= rightHeightPost + BLUR_HALF &&
				leftHeightPos >= rightHeightPost - BLUR_HALF) &&
				(rightHeightPost <= leftHeightPos + BLUR_HALF &&
				rightHeightPost >= leftHeightPos - BLUR_HALF))
		{
			int leftWidthPos = left.getWidthIndex();
			int rightWidthPos = right.getWidthIndex();
			int screenHalf = ARR_WIDTH / 2;
			
			// positions are the same:
			//   o
			// --|--
			//  / \
			//
			
			if (inGame)
			{
				// in game it does not matter, we just send forward
				return State.MOVEMENT_FORWARD;
			}
			// if not in game this has to be satisfied to start the game
			// this means that the user has to stand in the middle of the
			// camera record and streches his arms...
			else if (leftWidthPos < screenHalf - FORWARD_CHECK &&
					rightWidthPos > screenHalf + FORWARD_CHECK)
			{
				return State.MOVEMENT_FORWARD;
			}
			else 
				return null;
			
		}
		else if (leftHeightPos > rightHeightPost)
		{
			// left higher
			//  \o
			//   |\
			//  / \
			//
			return State.MOVEMENT_LEFT;
		}
		else
		{
			// right higher
			//   o/
			//  /|
			//  / \
			//
			return State.MOVEMENT_RIGHT;
		}
	}
	
	
	/**
	 * Converts the data array that we get from {@link ICameraDataProvider} to
	 * a matrix that one can actually print.
	 * 
	 * @param pixelsSingle the data array
	 * @param width the width of the matrix
	 * @param height the height of the matrix
	 * @return the matrix representation of the data array
	 */
	private float[][] convert2D(float[] pixelsSingle, int width,
			int height)
	{
		float[][] pixels = new float[width][height];
		
		int prevVal = -1;
		
		for (int i = 0; i < height; i++)
		{
			IMovementPoint mPointLeft = new MovementPoint();
			IMovementPoint mPointRight = new MovementPoint();
			prevVal = -1;
			
			for (int j = 0; j < width; j++)
			{
				
				
				// row wise fill
				pixels[j][i] = pixelsSingle[i * width + j];
				
				int currVal = (int)pixels[j][i];
				
				// case left
				if (currVal == 1 && prevVal == 0)
				{
					// only set this if the value was not set yet
					// has to be the furthest left!
					if (mPointLeft.getIntValue() == -1)
					{
						mPointLeft.setValues(j, i, currVal, pixels[j][i], 
								MovementPointType.LEFT);
					}
				}
				// case right
				else if (currVal == 0 && prevVal == 1)
				{
					// always set this, the further we go the better it gets...
					// note: set to prevVal
					mPointRight.setValues(j, i, prevVal, pixels[j][i], 
							MovementPointType.RIGHT);
				}
				
				prevVal = currVal;
			}
			
			// store the movement points in priority queue if they were set
			if (mPointLeft.getIntValue() != -1)
			{
				this.movementPointsLeft.add(mPointLeft);
				pixels[mPointLeft.getWidthIndex()][mPointLeft.getHeightIndex()] = 8;
			}
			if (mPointRight.getIntValue() != -1)
			{
				this.movementPointsRight.add(mPointRight);
				pixels[mPointRight.getWidthIndex()][mPointRight.getHeightIndex()] = 9;
			}
			
		}
		return pixels;
	}
	
	/**
	 * Fires a {@link StateChangeEvent} to all {@link IStateChangeEventListener}s
	 * that are registered to this data mapper.
	 * @param event the event to be fired
	 */
	private void fireEvent(StateChangeEvent event)
	{
		for (IStateChangeEventListener listener : listeners)
			listener.stateChanged(event);
	}
	
	@Override
	public void addStateChangeListener(IStateChangeEventListener listener)
	{
		if (!listeners.contains(listener))
			listeners.add(listener);
	}

	@Override
	public void removeStateChangeListener(IStateChangeEventListener listener)
	{
		listeners.remove(listener);
	}
	
	/**
	 * Writes a given matrix to the file "output.txt" in the project folder.
	 * @param matrix the matrix to be printed to the file
	 */
	private void printFloatMatrixToFile(float[][] matrix, boolean printfloat, String state)
	{
		File f = new File("./output.txt");
		if (!f.exists())
		{
			try
			{
				f.createNewFile();
			} catch (IOException e)
			{
				e.printStackTrace();
			}
		}
		
		FileWriter fos = null;
		BufferedWriter bos = null;
		
		try
		{
			fos  =  new FileWriter(f, true);
			bos =  new BufferedWriter(fos);
			
			
			bos.append("\n");
			for (int i = 0; i < ARR_HEIGHT; i++)
			{
				for (int j = 0; j < ARR_WIDTH; j++)
				{
					int bla = (int)matrix[j][i];
					if (bla == 0)
					{
						if (printfloat)
							bos.append("    ");
						else
							bos.append(" ");
					}
					else
					{
						if (printfloat)
							bos.append(String.format("%.1f", matrix[j][i]) + " ");
						else
							bos.append(bla + " ");
					}
				}
				bos.append("\n");
			}
			bos.append("\n" + state);
			bos.append("\n ##################################################################");
			
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		finally
		{
			try
			{
				fos.flush();
				//fos.close();
				bos.close();
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
		}
	}
	
	/**
	 * Prints a given matrix to the console.
	 * @param matrix the matrix to be printed.
	 */
	private void printFloatMatrix(float[][] matrix)
	{
		for (int i = 0; i < matrix.length; i++)
		{
			for (int j = 0; j < matrix[i].length; j++)
			{
				System.out.print((int)matrix[j][i] + " ");
			}
			System.out.println();
		}
	}
	
	@Override
	public void newData(EventObject event)
	{
		// new data arriving... analyze the data 
		//System.out.println(this.getClass().getName() + ": Got new Data :D");
		
		if (event instanceof DataEvent)
		{
			DataEvent de = (DataEvent)event;
//			System.out.println();
//			printFloatMatrix(de.getMatrix());
//			System.out.println();

			// clear previous movement points
			movementPointsLeft.clear();
			movementPointsRight.clear();
			
			// convert the pixel array to image matrix
			this.matrix = convert2D(de.getDataFrame(), ARR_WIDTH, ARR_HEIGHT);
			
			IMovementPoint mPointRightFinal = null;
			IMovementPoint mPointLeftFinal = null;
			
			// get the best left movement point
			while (!movementPointsLeft.isEmpty())
			{
				IMovementPoint candidate = movementPointsLeft.remove();
//				System.out.println("Left: " + candidate.getWidthIndex());
				
				if (analyzeMovementPointCandidate(candidate))
				{
					mPointLeftFinal = candidate;
//					System.out.println("FOUND LEFT CANDIDATE");
					break;
				}
			}
			// get the best right movement point
			while (!movementPointsRight.isEmpty())
			{
				IMovementPoint candidate = movementPointsRight.remove();
//				System.out.println("Right: " + candidate.getWidthIndex());
				
				if (analyzeMovementPointCandidate(candidate))
				{
					mPointRightFinal = candidate;
//					System.out.println("FOUND RIGHT CANDIDATE");
					break;
				}
			}
			
			if (mPointLeftFinal == null || mPointRightFinal == null)
			{
//				System.out.println("State not detected...");
				printFloatMatrixToFile(matrix, false, "Could not detect state...");
				
				if (retryCounter >= INGAME_RETRIES)
				{
					inGame = false;
					if (prevState != State.SHOW_TUTORIAL)
					{
						prevState = State.SHOW_TUTORIAL;
						fireEvent(new StateChangeEvent(this, State.SHOW_TUTORIAL));
					}
				}
				retryCounter++;
				
				return;
			}
			
			// map the movement point to the state
			State currState = mapMovementPointsToState(mPointLeftFinal, 
					mPointRightFinal);
			
			if (!inGame && startGameCounter >= START_GAME_BUFF &&
					currState == State.MOVEMENT_FORWARD)
			{
				inGame = true;
				startGameCounter = 0;
				retryCounter = 0;
				fireEvent(new StateChangeEvent(this, State.SHOW_GAME));
				currState = State.SHOW_GAME;
				prevState = State.SHOW_GAME;
			}
			else if (!inGame &&
					currState == State.MOVEMENT_FORWARD)
			{
				startGameCounter++;
			}
			else if (!inGame)
			{
				startGameCounter = 0;
			}
			
			if (inGame && currState != null)
			{
				// if the state changed fire a state change event that will notify
				// the key simulator
				if (currState != prevState)
				{
					prevState = currState;
					fireEvent(new StateChangeEvent(this, currState));
//					System.out.println(this.getClass().getSimpleName() + " STATE changed to: " + currState);
				}
			}
			
			
		}
		// print matrix to file
		if (prevState != null)
			printFloatMatrixToFile(matrix, false, prevState.toString());
	}


}
